/**
 * Author:yuiop
 * Date:2022/8/30 15:31
 */
package com.devops.admin.service.impl;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.devops.admin.mapper.DOSMetricsMapper;
import com.devops.admin.model.*;

import com.devops.admin.service.*;
import com.devops.admin.util.Assert;
import com.devops.admin.util.DOSPriorityQueue;
import com.devops.admin.util.LettuceRedisUtils;
import com.devops.admin.vo.DOSMetricsVo;
import com.devops.admin.vo.DOSPageVo;
import com.devops.shiro.vm.BaseUserVM;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.prometheus.client.Collector;
import io.prometheus.client.CounterMetricFamily;
import io.prometheus.client.GaugeMetricFamily;
import io.prometheus.client.exporter.common.TextFormat;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

@Service
@Slf4j
public class DOSMetricsServiceImpl extends BaseServiceImpl<DOSMetricsMapper, DOSMetrics> implements DOSMetricsService {
    @Autowired
    DOSDynamicSQLService dosDynamicSQLService;
    @Autowired
    DOSMetricsDbService dosMetricsDbService;
    @Autowired
    DOSMetricsRelationService dosMetricsRelationService;
    @Autowired
    DOSResourceService dosResourceService;
    @Autowired
    DOSDatasourceService dosDatasourceService;

    /**
     * 根据查询条件从数据库中获取数据、动态执行sql获取结果、处理输数据格式如：List<Collector.MetricFamilySamples>
     * @param lambdaQueryWrapper
     * @return
     */
    public List<Collector.MetricFamilySamples> dealMetrics(LambdaQueryWrapper<DOSMetrics> lambdaQueryWrapper) {
        List<DOSMetrics> dosMetrics = baseMapper.selectList(lambdaQueryWrapper);
        return formatMetrics(dosMetrics);
    }

    /**
     * 动态执行sql并将结果转换成 List<Collector.MetricFamilySamples>
     * @param dosMetrics
     * @return
     */
    public List<Collector.MetricFamilySamples> formatMetrics(List<DOSMetrics> dosMetrics) {
        List<Collector.MetricFamilySamples> metrics = new ArrayList<>();
        if (dosMetrics != null) {
            CompletableFuture.allOf(dosMetrics.stream()
                    .map(dosMetric -> CompletableFuture.runAsync(() -> collect(dosMetric, metrics))).toArray(CompletableFuture[]::new)).join();
        }
        return metrics;
    }

    private void collect(DOSMetrics dosMetric, List<Collector.MetricFamilySamples> metrics) {
        String name = dosMetric.getName();
        String help = dosMetric.getHelp();
        String type = dosMetric.getType();
        String valueKey = dosMetric.getValueKey();
        String dbName = dosMetric.getDbName();
        String dbSql = dosMetric.getDbSql();
        Integer redisId = dosMetric.getRedisId();
        String redisKey = dosMetric.getRedisKey();
        Integer redisDb = dosMetric.getRedisDb();
        String labelsKey = dosMetric.getLabelsKey();
        // 这种情况不做处理，直接返回
        if (StrUtil.isNotBlank(labelsKey) && StrUtil.isBlank(valueKey)){
            return;
        }
        if (dosMetric.getDataSourceType() == 1) {
            if (StrUtil.isBlank(name) || StrUtil.isBlank(help) || StrUtil.isBlank(type)
                     || StrUtil.isBlank(dbName) || StrUtil.isBlank(dbSql)) {
                return;
            }
        } else if (dosMetric.getDataSourceType() == 2){
            if (StrUtil.isBlank(name) || StrUtil.isBlank(help) || StrUtil.isBlank(type)
                     || StrUtil.isBlank(redisKey) || redisId == null || redisDb == null) {
                return;
            }
        }
        List<Map<String,Object>> resultList = new ArrayList<>();
        //没有labelsKey 也没有 valueKey 需要解析的就是一个数值
        if (StrUtil.isBlank(labelsKey) && StrUtil.isBlank(valueKey)) {
            if (dosMetric.getDataSourceType() == 1){
                if (StrUtil.isBlank(dbName)){
                    return;
                }
                Assert.isTrueMvc(dosDatasourceService.checkDB(dbName),"数据库异常，请联系管理员");

                resultList = dosDynamicSQLService.executeSql(dbName,dbSql);
                if (CollectionUtil.isNotEmpty(resultList)) { //数据库中有结果集
                    Map<String,Object> resultMap = resultList.get(0); //取第一条 期望值就只有一条 如果有多条数据 理论上是错误数据 这里暂不处理
                    Double resultValue = null;
                    //遍历map的value，找到一个能转成double类型的value，这里不确保精准
                    for (Map.Entry<String,Object> entry : resultMap.entrySet()) {
                        try {
                            resultValue = Double.parseDouble(entry.getValue().toString());
                            break;
                        } catch (NumberFormatException e) {
                            log.error("类型转换出错了:{}",entry.getValue().toString());
                        }
                    }
                    //如果遍历完map后找到一个double类型的value 封装指标
                    if (resultValue != null) {
                        if (type.equals("counter")) {
                            metrics.add(new CounterMetricFamily(name, help, resultValue));
                        } else if (type.equals("gauge")) {
                            metrics.add(new GaugeMetricFamily(name, help, resultValue));
                        }
                    }
                }
            } else if (dosMetric.getDataSourceType() == 2){
                // 获取redis指标信息
                getRedisMetrics(redisId, redisDb, redisKey, resultList,1);
                if (CollectionUtil.isNotEmpty(resultList) && resultList.size() == 1) {
                    Map<String,Object> resultMap = resultList.get(0);
                    double resultValue = Double.parseDouble(resultMap.get("value").toString());
                    //执行到这里没有报错 代表数据格式已经转换成功了
                    if (type.equals("counter")) {
                        metrics.add(new CounterMetricFamily(name, help, resultValue));
                    } else if (type.equals("gauge")) {
                        metrics.add(new GaugeMetricFamily(name, help, resultValue));
                    }
                }
            }
        } else if (StrUtil.isNotBlank(labelsKey) && StrUtil.isBlank(valueKey)) { //有labelsKey 没有valueKey 暂时我们不支持 前端做限制

        } else { // 有valueKey 可能有labelsKey 我们需要处理的是一个List
            if (dosMetric.getDataSourceType() == 1){
                if (StrUtil.isBlank(dbName)){
                    return;
                }
                Assert.isTrueMvc(dosDatasourceService.checkDB(dbName),"数据库异常，请联系管理员");
                resultList = dosDynamicSQLService.executeSql(dbName,dbSql);
            } else if (dosMetric.getDataSourceType() == 2){
                // 获取redis指标信息
                getRedisMetrics(redisId, redisDb, redisKey, resultList,2);
            }
            if (resultList != null && resultList.size() > 0) { //当结果集不为空的时候
                //如果有labelsKey 需要遍历结果集
                if (StrUtil.isNotBlank(labelsKey)) {
                    List<String> labelKeys = Arrays.asList(labelsKey.split(","));
                    Collector.MetricFamilySamples sample;
                    if (type.equals("counter")) {
                        sample = new CounterMetricFamily(name, help,labelKeys);
                    } else if (type.equals("gauge")) {
                        sample = new GaugeMetricFamily(name, help,labelKeys);
                    } else {
                        return;
                    }
                    resultList.forEach(map -> {
                        ArrayList<String> labelValues = new ArrayList<>();
                        labelKeys.forEach(labelKey -> {
                            String labelValue = dealMapValueToString(map,labelKey);
                            if (labelValue != null) {
                                labelValues.add(labelValue);
                            }
                        });
                        if (labelValues.size() > 0) {
                            Double value = dealMapValueToDouble(map,valueKey);
                            if (value != null) {
                                if (sample instanceof CounterMetricFamily) {
                                    ((CounterMetricFamily) sample).addMetric(labelValues,value);
                                } else {
                                    ((GaugeMetricFamily) sample).addMetric(labelValues,value);
                                }

                            }
                        }
                    });
                    metrics.add(sample);
                } else { //没有labelsKey 取结果集的第一条
                    Map<String,Object> resultMap = resultList.get(0);
                    if (resultMap != null) {
                        Double value = dealMapValueToDouble(resultMap, valueKey);
                        if (value != null) {
                            if (type.equals("counter")) {
                                metrics.add(new CounterMetricFamily(name, help, value));
                            } else if (type.equals("gauge")) {
                                metrics.add(new GaugeMetricFamily(name, help, value));
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * @Author heyabo
     * @Description 获取redis的指标信息
     * @Date 11:19 2022/9/22
     * @Param [redisId, redisDb, resultList]
     * @return void
     **/
    private void getRedisMetrics(Integer redisId, Integer redisDb, String redisKey,
                                 List<Map<String, Object>> resultList,Integer resultType) {
        // 获取redis的连接信息
        DOSResource resource = dosResourceService.getById(redisId);
        if (resource == null){
            return;
        }
        // 从自定义的队列中获取redis连接
        DOSPriorityQueue priorityQueue = DOSPriorityQueue.getInstance();
        RedisClientOps client = priorityQueue.getClient(String.valueOf(redisId)+redisDb);
        if (client == null) {
            // 获取redis连接的客户端
            RedisClient redisClient = LettuceRedisUtils.getRedisUri(resource, redisDb);
            // 将redis客户端和生成的连接放入自定义的队列中
            client = RedisClientOps.builder()
                    .client(redisClient)
                    .connect(redisClient.connect())
                    .level(3).build();
            priorityQueue.insert(String.valueOf(redisId)+redisDb, client);
        }
        // 从redis获取指标的信息
        RedisAsyncCommands<String, String> commands = client.getConnect().async();
        try {
            String value = commands.get(redisKey).get();
            if (resultType == 1) {
                HashMap<String,Object> map = new HashMap<>();
                map.put("value",value);
                resultList.add(map);
            } else if (resultType == 2) {
                //取回一个List<String>
                JSONArray jsonArray = JSONUtil.parseArray(value);
                //解析数据
                exchangeToMap(jsonArray,resultList);
            }

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }

    }

    /**
     * 处理List<Collector.MetricFamilySamples>格式数据为Prometheus标准的String格式
     * @param sampleList
     * @return
     * @throws IOException
     */
    public String formatMetricsToString(List<Collector.MetricFamilySamples> sampleList) throws IOException {
        if (sampleList != null && sampleList.size() > 0) {
            Writer writer = new StringWriter();
            TextFormat.write004(writer, Collections.enumeration(sampleList));
            return writer.toString();
        } else {
            return "";
        }

    }

    /**
     * 处理map获取value
     * valueKey valueKey转成全小写 valueKey转成全大写
     * @param map
     * @param valueKey
     * @return
     */
    public Double dealMapValueToDouble(Map<String,Object> map,String valueKey) {
        Double result = null;
        try {
            if (map.get(valueKey) != null) {
                result = Double.parseDouble(map.get(valueKey).toString());
            } else if (map.get(valueKey.toLowerCase()) != null) {
                result = Double.parseDouble(map.get(valueKey.toLowerCase()).toString());
            } else if (map.get(valueKey.toUpperCase()) != null) {
                result = Double.parseDouble(map.get(valueKey.toUpperCase()).toString());
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 处理map获取label
     * labelKey labelKey转成全小写 labelKey转成全大写
     * @param map
     * @param labelKey
     * @return
     */
    public String dealMapValueToString(Map<String,Object> map,String labelKey) {
        String result = null;
        if (map.get(labelKey) != null) {
            result = map.get(labelKey).toString();
        } else if (map.get(labelKey.toLowerCase()) != null) {
            result = map.get(labelKey.toLowerCase()).toString();
        } else if (map.get(labelKey.toUpperCase()) != null) {
            result = map.get(labelKey.toUpperCase()).toString();
        }
        return result;
    }

    /**
     * dosMetricsVo 校验
     * @param dosMetricsVo
     */
    public void checkMetricsVoInSave(DOSMetricsVo dosMetricsVo) {
        Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getName()),"指标名称不能为空");
        Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getHelp()),"指标help不能为空");
        Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getType()),"指标类型不能为空");

        Assert.isTrueMvc(!(StrUtil.isNotBlank(dosMetricsVo.getLabelsKey())
                && StrUtil.isBlank(dosMetricsVo.getValueKey())),"请设置valueKey");

        if (dosMetricsVo.getDataSourceType() == 1) {
            Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getDbName()), "指标对应dbName不能为空");
            Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getDbSql()), "指标对应的dbSql不能为空");
            Assert.isTrue(dosMetricsVo.getDbId() != null
                    && dosMetricsVo.getDbId() > 0, "数据库id不能为空");
            // 校验数据库是否合法
            DOSResource resource = dosResourceService.getById(dosMetricsVo.getDbId());
            Assert.isTrue(resource != null && resource.getId() > 0
                    && resource.getName().equals(dosMetricsVo.getDbName()),"指标对应的dbId和dbName不合法");
        } else if (dosMetricsVo.getDataSourceType() == 2){
            Assert.isTrue(StrUtil.isNotBlank(dosMetricsVo.getRedisKey()),"redisKey不能为空");
            Assert.isTrue(dosMetricsVo.getRedisId() != null
                    && dosMetricsVo.getRedisId() > 0, "redis的id不能为空");
            Assert.isTrue(dosMetricsVo.getRedisDb() != null
                    && dosMetricsVo.getRedisDb() >= 0, "redis的数据库id不能为空");
        }
    }


    @Override
    public List<Collector.MetricFamilySamples> getAllMetrics() {
        return dealMetrics(Wrappers.lambdaQuery());
    }

    @Override
    public String getMetricsByName(String metricsName) throws IOException {
        List<Collector.MetricFamilySamples> sampleList = dealMetrics(Wrappers.<DOSMetrics>lambdaQuery().eq(DOSMetrics::getName,metricsName));
        return formatMetricsToString(sampleList);
    }

    @Override
    public String testMetrics(DOSMetricsVo dosMetricsVo) throws IOException{
        //dosMetricsVo 校验
        checkMetricsVoInSave(dosMetricsVo);
        DOSMetrics dosMetrics = BeanUtil.copyProperties(dosMetricsVo, DOSMetrics.class);
        List<DOSMetrics> metricsList = new ArrayList<>();
        metricsList.add(dosMetrics);
        List<Collector.MetricFamilySamples> sampleList = formatMetrics(metricsList);
        return formatMetricsToString(sampleList);
    }



    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveMetrics(DOSMetricsVo dosMetricsVo) {
        //dosMetricsVo 校验
        checkMetricsVoInSave(dosMetricsVo);

        // 获取当前登录的人员信息
        BaseUserVM user = (BaseUserVM) SecurityUtils.getSubject().getPrincipal();

        //数据库里校验指标名称
        Assert.isTrue(count(Wrappers.<DOSMetrics>lambdaQuery().eq(DOSMetrics::getName,dosMetricsVo.getName())) == 0,
                "指标名称不能重复");

        DOSMetrics dosMetrics = BeanUtil.copyProperties(dosMetricsVo, DOSMetrics.class);
        // 保存指标
        boolean saveMetrics = this.save(dosMetrics);

        DOSMetricsRelation relation = DOSMetricsRelation.builder()
                .metricsId(dosMetrics.getId())
                .userId(user.getId())
                .build();
        // 保存指标与人员、项目关联表
        boolean saveRelation = dosMetricsRelationService.save(relation);

        return saveMetrics == saveRelation;
    }

    @Override
    public TableResult<DOSMetrics> getPage(DOSMetricsVo dosMetricsVo, DOSPageVo pageVo) {
        // 获取当前登录的人员信息
        BaseUserVM user = (BaseUserVM) SecurityUtils.getSubject().getPrincipal();
        Page<DOSMetrics> page = new Page<>(pageVo.getPageNum(),pageVo.getPageSize());
        List<DOSMetrics> metrics;
        if (user.getAdminRole()){
            // 管理员可以看到所有的指标
            metrics = baseMapper.getPage(dosMetricsVo,page,null);
        }else {
            // 非管理员只能看到自己创建的指标
            metrics = baseMapper.getPage(dosMetricsVo,page,user.getId());
        }
        metrics.forEach(metric->{
            if (metric.getDbId() != null && metric.getDbId() > 0){
                DOSResource resource = dosResourceService.getById(metric.getDbId());
                if (resource != null) {
                    metric.setDbType(resource.getSubType());
                }
            }
        });
        TableResult<DOSMetrics> tableResult = new TableResult<>();
        // 组装分页数据
        Pagination p = Pagination.builder().pageNum(page.getCurrent()).pageSize(page.getSize()).total(page.getTotal()).build();
        tableResult.setPagination(p);
        tableResult.setList(metrics);

        return tableResult;
    }

    @Override
    public boolean updateMetrics(DOSMetricsVo dosMetricsVo) {
        Assert.isTrue(dosMetricsVo.getId() != null && dosMetricsVo.getId() > 0,"指标id不能为空");
        // 校验dbId是否合法
        if (dosMetricsVo.getDbId() != null && dosMetricsVo.getDataSourceType() == 1){
            // 校验数据库是否合法
            DOSResource resource = dosResourceService.getById(dosMetricsVo.getDbId());
            Assert.isTrue(resource != null && resource.getId() > 0
                    && resource.getName().equals(dosMetricsVo.getDbName()),"指标对应的dbId和dbName不合法");
        }

        DOSMetrics metrics = BeanUtil.copyProperties(dosMetricsVo,DOSMetrics.class);
        // 根据id更新指标信息
        return this.updateById(metrics);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean removeMetricsById(Integer metricsId) {
        LambdaQueryWrapper<DOSMetricsRelation> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DOSMetricsRelation::getMetricsId,metricsId);
        // 删除指标与人员的关系
        dosMetricsRelationService.remove(queryWrapper);

        // 删除指标信息
        return this.removeById(metricsId);
    }

    /**
     * @Author heyabo
     * @Description 解析JSONArray类型的数据为List<Map<String,Object>>
     * @Date 11:14 2022/9/25
     * @Param [array, resultList]
     * @return void
     **/
    private void exchangeToMap(JSONArray array, List<Map<String,Object>> resultList) {
        for (Object obj : array) {
            // 如果obj的数据类型是JSONArray则递归调用方法
            if (obj instanceof JSONArray){
                exchangeToMap(JSONUtil.parseArray(obj.toString()),resultList);
            }
            // 如果为JSONObject类型则转换为map类型
            if (obj instanceof JSONObject){
                Map<String,Object> map = (Map<String, Object>) JSONUtil.parse(obj.toString());
                map.forEach((key,value)->{
                    // 处理数据中数据类型多余出来的字符串
                    if (value instanceof JSONArray){
                        JSONArray jsonArray = JSONUtil.parseArray(value.toString());
                        if (jsonArray.size() == 2 && jsonArray.get(0) instanceof String){
                            map.put(key,jsonArray.get(1));
                        }
                    }
                });

                resultList.add(map);
            }
        }
    }
}
